/*
 * Copyright (c) 2006, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.util.zip;

import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;

/**
 * Implements an input stream filter for compressing data in the "deflate"
 * compression format.
 *
 * @author David R Tribble (david@tribble.com)
 * @see DeflaterOutputStream
 * @see InflaterOutputStream
 * @see InflaterInputStream
 * @since 1.6
 */

public class DeflaterInputStream extends FilterInputStream {

  /**
   * Compressor for this stream.
   */
  protected final Deflater def;

  /**
   * Input buffer for reading compressed data.
   */
  protected final byte[] buf;

  /**
   * Temporary read buffer.
   */
  private byte[] rbuf = new byte[1];

  /**
   * Default compressor is used.
   */
  private boolean usesDefaultDeflater = false;

  /**
   * End of the underlying input stream has been reached.
   */
  private boolean reachEOF = false;

  /**
   * Check to make sure that this stream has not been closed.
   */
  private void ensureOpen() throws IOException {
    if (in == null) {
      throw new IOException("Stream closed");
    }
  }

  /**
   * Creates a new input stream with a default compressor and buffer
   * size.
   *
   * @param in input stream to read the uncompressed data to
   * @throws NullPointerException if {@code in} is null
   */
  public DeflaterInputStream(InputStream in) {
    this(in, new Deflater());
    usesDefaultDeflater = true;
  }

  /**
   * Creates a new input stream with the specified compressor and a
   * default buffer size.
   *
   * @param in input stream to read the uncompressed data to
   * @param defl compressor ("deflater") for this stream
   * @throws NullPointerException if {@code in} or {@code defl} is null
   */
  public DeflaterInputStream(InputStream in, Deflater defl) {
    this(in, defl, 512);
  }

  /**
   * Creates a new input stream with the specified compressor and buffer
   * size.
   *
   * @param in input stream to read the uncompressed data to
   * @param defl compressor ("deflater") for this stream
   * @param bufLen compression buffer size
   * @throws IllegalArgumentException if {@code bufLen <= 0}
   * @throws NullPointerException if {@code in} or {@code defl} is null
   */
  public DeflaterInputStream(InputStream in, Deflater defl, int bufLen) {
    super(in);

    // Sanity checks
    if (in == null) {
      throw new NullPointerException("Null input");
    }
    if (defl == null) {
      throw new NullPointerException("Null deflater");
    }
    if (bufLen < 1) {
      throw new IllegalArgumentException("Buffer size < 1");
    }

    // Initialize
    def = defl;
    buf = new byte[bufLen];
  }

  /**
   * Closes this input stream and its underlying input stream, discarding
   * any pending uncompressed data.
   *
   * @throws IOException if an I/O error occurs
   */
  public void close() throws IOException {
    if (in != null) {
      try {
        // Clean up
        if (usesDefaultDeflater) {
          def.end();
        }

        in.close();
      } finally {
        in = null;
      }
    }
  }

  /**
   * Reads a single byte of compressed data from the input stream.
   * This method will block until some input can be read and compressed.
   *
   * @return a single byte of compressed data, or -1 if the end of the uncompressed input stream is
   * reached
   * @throws IOException if an I/O error occurs or if this stream is already closed
   */
  public int read() throws IOException {
    // Read a single byte of compressed data
    int len = read(rbuf, 0, 1);
    if (len <= 0) {
      return -1;
    }
    return (rbuf[0] & 0xFF);
  }

  /**
   * Reads compressed data into a byte array.
   * This method will block until some input can be read and compressed.
   *
   * @param b buffer into which the data is read
   * @param off starting offset of the data within {@code b}
   * @param len maximum number of compressed bytes to read into {@code b}
   * @return the actual number of bytes read, or -1 if the end of the uncompressed input stream is
   * reached
   * @throws IndexOutOfBoundsException if {@code len > b.length - off}
   * @throws IOException if an I/O error occurs or if this input stream is already closed
   */
  public int read(byte[] b, int off, int len) throws IOException {
    // Sanity checks
    ensureOpen();
    if (b == null) {
      throw new NullPointerException("Null buffer for read");
    } else if (off < 0 || len < 0 || len > b.length - off) {
      throw new IndexOutOfBoundsException();
    } else if (len == 0) {
      return 0;
    }

    // Read and compress (deflate) input data bytes
    int cnt = 0;
    while (len > 0 && !def.finished()) {
      int n;

      // Read data from the input stream
      if (def.needsInput()) {
        n = in.read(buf, 0, buf.length);
        if (n < 0) {
          // End of the input stream reached
          def.finish();
        } else if (n > 0) {
          def.setInput(buf, 0, n);
        }
      }

      // Compress the input data, filling the read buffer
      n = def.deflate(b, off, len);
      cnt += n;
      off += n;
      len -= n;
    }
    if (cnt == 0 && def.finished()) {
      reachEOF = true;
      cnt = -1;
    }

    return cnt;
  }

  /**
   * Skips over and discards data from the input stream.
   * This method may block until the specified number of bytes are read and
   * skipped. <em>Note:</em> While {@code n} is given as a {@code long},
   * the maximum number of bytes which can be skipped is
   * {@code Integer.MAX_VALUE}.
   *
   * @param n number of bytes to be skipped
   * @return the actual number of bytes skipped
   * @throws IOException if an I/O error occurs or if this stream is already closed
   */
  public long skip(long n) throws IOException {
    if (n < 0) {
      throw new IllegalArgumentException("negative skip length");
    }
    ensureOpen();

    // Skip bytes by repeatedly decompressing small blocks
    if (rbuf.length < 512) {
      rbuf = new byte[512];
    }

    int total = (int) Math.min(n, Integer.MAX_VALUE);
    long cnt = 0;
    while (total > 0) {
      // Read a small block of uncompressed bytes
      int len = read(rbuf, 0, (total <= rbuf.length ? total : rbuf.length));

      if (len < 0) {
        break;
      }
      cnt += len;
      total -= len;
    }
    return cnt;
  }

  /**
   * Returns 0 after EOF has been reached, otherwise always return 1.
   * <p>
   * Programs should not count on this method to return the actual number
   * of bytes that could be read without blocking
   *
   * @return zero after the end of the underlying input stream has been reached, otherwise always
   * returns 1
   * @throws IOException if an I/O error occurs or if this stream is already closed
   */
  public int available() throws IOException {
    ensureOpen();
    if (reachEOF) {
      return 0;
    }
    return 1;
  }

  /**
   * Always returns {@code false} because this input stream does not support
   * the {@link #mark mark()} and {@link #reset reset()} methods.
   *
   * @return false, always
   */
  public boolean markSupported() {
    return false;
  }

  /**
   * <i>This operation is not supported</i>.
   *
   * @param limit maximum bytes that can be read before invalidating the position marker
   */
  public void mark(int limit) {
    // Operation not supported
  }

  /**
   * <i>This operation is not supported</i>.
   *
   * @throws IOException always thrown
   */
  public void reset() throws IOException {
    throw new IOException("mark/reset not supported");
  }
}
